﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Pfz.Threading;

#if SILVERLIGHT
using System.Windows;
#endif

namespace Pfz.Caching
{
	/// <summary>
	/// Some methods and events to interact with garbage collection. You can 
	/// keep an object alive during the next collection or register to know 
	/// when a collection has just happened. This is useful if you don't use
	/// WeakReferences, but know how to free memory if needed. For example, 
	/// you can do a TrimExcess to your lists to free some memory.
	/// 
	/// Caution: GC.KeepAlive keeps the object alive until that line of code,
	/// while GCUtils.KeepAlive keeps the object alive until the next 
	/// generation.
	/// </summary>
	public static class GCUtils
    {
		#region Constructor
			private static bool _finished;
			private static readonly object _collectedEvent = new object();
			[SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
			static GCUtils()
			{
				#if SILVERLIGHT
					Application.Current.Exit += _ProcessExit;
				#else
					AppDomain current = AppDomain.CurrentDomain;
					current.DomainUnload += _ProcessExit;
					current.ProcessExit += _ProcessExit;
				#endif
				var runner = new Runner();
				runner.DoNothing();
				
				Thread collectorThread = new Thread(_ExecuteCollected);
				collectorThread.Name = "Pfz.Caching.GCUtils.Collected executor thread.";

				#if !SILVERLIGHT
					collectorThread.Priority = ThreadPriority.AboveNormal;
				#endif

				collectorThread.Start();
			}
		#endregion
		#region Finalization event handles
			private static void _ProcessExit(object sender, EventArgs e)
			{
				_finished = true;

				lock(_collectedEvent)
					Monitor.Pulse(_collectedEvent);
			}
		#endregion
		
		#region ProcessMemory
			private static long _processMemory;
			
			/// <summary>
			/// Gets or sets a value that indicates how much memory the process
			/// can use without freeing it's caches.
			/// Note that such value does not affect how often GC occurs and is
			/// not the size of the cache, it only says: If my process is not using
			/// more than X memory, caches don't need to be erased.
			/// The default value is 0 mb.
			/// </summary>
			public static long ProcessMemory
			{
				get
				{
					return _processMemory;
				}
				set
				{
					_processMemory = value;
				}
			}
		#endregion
    
		#region KeepAlive
			#if SILVERLIGHT
				private static HashSet<object> _keepedObjects = new HashSet<object>(ReferenceComparer<object>.Instance);
			#else
				private static HashSet<object> _keepedObjects = new HashSet<object>(ReferenceComparer.Instance);
			#endif
			
			/// <summary>
			/// Keeps an object alive at the next collection. This is useful for WeakReferences as an way
			/// to guarantee that recently used objects will not be immediatelly collected. At the next
			/// generation, you can call KeepAlive again, making the object alive for another generation.
			/// </summary>
			/// <param name="item"></param>
			public static void KeepAlive(object item)
			{
				if (item == null)
					return;
			
				var keepedObjects = _keepedObjects;
				
				lock(keepedObjects)
					keepedObjects.Add(item);
			}
		#endregion
		#region Expire
			/// <summary>
			/// Expires an object. Is the opposite of KeepAlive.
			/// </summary>
			/// <param name="item"></param>
			/// <returns>true if the object was in the KeepAlive list, false otherwise.</returns>
			public static bool Expire(object item)
			{
				if (item == null)
					return false;
			
				var keepedObjects = _keepedObjects;

				bool result;
				lock(keepedObjects)
					result = keepedObjects.Remove(item);

				return result;
			}
		#endregion
		#region ExpireAll
			/// <summary>
			/// Expires all items in a collection using a single lock.
			/// </summary>
			/// <param name="collection">The collection of items to expire.</param>
			public static void ExpireAll(IEnumerable<object> collection)
			{
				if (collection == null)
					throw new ArgumentNullException("collection");

				var keepedObjects = _keepedObjects;
				lock(keepedObjects)
					foreach(var item in collection)
						keepedObjects.Remove(item);
			}
		#endregion

		#region _ExecuteCollected
			private static void _ExecuteCollected()
			{
				var thread = Thread.CurrentThread;
				while(true)
				{
					// we are background while waiting.
					thread.IsBackground = true;
					
					lock(_collectedEvent)
					{
						if (_finished)
							return;

						Monitor.Wait(_collectedEvent);
					}
					
					if (_finished)
						return;
						
					// but we are not background while running.
					thread.IsBackground = false;
					
					_ExecuteCollectedNow();
				}
			}
		#endregion
		#region _ExecuteCollectedNow
			private static void _ExecuteCollectedNow()
			{
				#if SILVERLIGHT
					_keepedObjects = new HashSet<object>(ReferenceComparer<object>.Instance);
				#else
					_keepedObjects = new HashSet<object>(ReferenceComparer.Instance);
				#endif

				Thread.MemoryBarrier();

				((IGarbageCollectionAware)_gcAwareSet).OnCollected();
				var gcAwareSet = _gcAwareSet.ToList();
				int count2 = gcAwareSet.Count;
				for(int i=0; i<count2; i++)
					gcAwareSet[i].OnCollected();

			}
		#endregion
		
		#region RegisterForCollectedNotification
			private static WeakHashSet<IGarbageCollectionAware> _gcAwareSet = new WeakHashSet<IGarbageCollectionAware>(false);
			/// <summary>
			/// Registers the given object to the Collected notification.
			/// </summary>
			public static void RegisterForCollectedNotification(IGarbageCollectionAware gcAware)
			{
				if (gcAware == null)
					return;

				_gcAwareSet.Add(gcAware);
			}
		#endregion
		#region UnregisterFromCollectedNotification
			/// <summary>
			/// Unregisters the given object from the Collected notification.
			/// </summary>
			public static void UnregisterFromCollectedNotification(IGarbageCollectionAware gcAware)
			{
				if (gcAware == null)
					return;

				_gcAwareSet.Remove(gcAware);
			}
		#endregion
        
        #region Runner - nested class
			private sealed class Runner
			{
				[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
				public void DoNothing()
				{
				}

				~Runner()
				{
					// If we don't test, we will keep re-registering forever
					// when the application is finishing.
					if (_finished)
						return;
						
					GC.ReRegisterForFinalize(this);
					
					if (GC.GetTotalMemory(false) <= _processMemory)
						return;
					
					// does not need to be thread-safe, as this is the
					// destructor thread, but if the lock is not got
					// we should wait the next collection, as we
					// can't block while all threads may be suspended.
					if (Monitor.TryEnter(_collectedEvent, 0))
					{
						try
						{
							Monitor.Pulse(_collectedEvent);
						}
						finally
						{
							Monitor.Exit(_collectedEvent);
						}
					}
				}
			}
        #endregion
    }
}
